Skip to content

Commit 1816f4d

Browse files
authored
support for bash like prompt and groups command (#12)
* support for bash like prompt and groups command * banner update
1 parent 0d136a9 commit 1816f4d

File tree

7 files changed

+573
-17
lines changed

7 files changed

+573
-17
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ There are 3 possible usecases:
2424
- you can easily add any other HDFS manipulation function
2525
- there is a command history persisting in history log (~/.hdfs-shell/hdfs-shell.log)
2626
- **support for relative directory + commands ```cd``` and ```pwd```**
27+
- advanced commands like ```su```, ```groups```, ```whoami```
28+
- customizable shell prompt
2729

2830
#### Disadvantages UI against direct calling hdfs dfs function:
2931

@@ -72,13 +74,21 @@ For our purposes we also integrated following commands:
7274
- ```cd```, ```pwd```
7375
- ```su <username>``` - ***experimental*** - changes current active user - it won't probably work on secured HDFS (KERBEROS)
7476
- ```whoami``` - prints effective username
77+
- ```groups <username1 <username2,...>>``` - eg.```groups hdfs``` prints groups for given users, same as ```hdfs groups my_user my_user2``` functionality
7578
- ```edit 'my file'``` - see the config below
7679

7780

7881
###### Edit Command
7982
Since the version 1.0.4 the simple command 'edit' is available. The command gets selected file from HDFS to the local temporary directory and launches the editor. Once the editor saves the file (with a result code 0), the file is uploaded back into HDFS (target file is overwritten).
8083
By default the editor path is taken from ```$EDITOR``` environment variable. If ```$EDITOR``` is not set, ```vim``` (Linux, Mac) or ```notepad.exe``` (Windows) is used.
8184

85+
###### How to change command (shell) prompt
86+
HDFS Shell supports customized bash-like prompt setting!
87+
I implemented support for these switches listed in this [table](https://bash.cyberciti.biz/guide/Changing_bash_prompt) (include colors!, exclude ```\!, \#```).
88+
You can also use this [online prompt generator](http://ezprompt.net/) to create prompt value of your wish.
89+
To setup your favorite prompt simply add ```export HDFS_SHELL_PROMPT="value"``` to your .bashrc (or set env variable on Windows) and that's it. Restart HDFS Shell to apply change.
90+
Default value is currently set to ```\e[36m\u@\h \e[0;39m\e[33m\w\e[0;39m\e[36m\\$ \e[37;0;39m```.
91+
8292
### Running Daemon mode
8393
![Image of HDFS-Shell](https://github.com/avast/hdfs-shell/blob/master/web/screenshot2.png)
8494

@@ -101,7 +111,7 @@ For developing, add to JVM args in your IDE launch config dialog:
101111

102112
#### Known limitations & problems
103113

104-
- There is a problem with a parsing of commands containing a file or directory including a space - eg. it's not possible to create directory ```My dir``` using command ```mkdir "My dir"``` . This would be probably resolved with an upgrade to Spring Shell 2.
114+
- There is a problem with a parsing of commands containing a file or directory including a space - eg. it's not possible to create directory ```My dir``` using command ```mkdir "My dir"``` . This should be probably resolved with an upgrade to Spring Shell 2.
105115
- It's not possible to remove root directory (```rm -R dir```) from root (```/```) directory. You have to use absolut path instead (```rm -R /dir```). It's caused by bug in Hadoop. See [HADOOP-15233](https://issues.apache.org/jira/browse/HADOOP-15233) for more details. Removing directory from another cwd is not affected.
106116

107117
### Contact

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
springShellVersion = 1.2.0.RELEASE
22
theGroup=com.avast.server
3-
theVersion=1.0.6
3+
theVersion=1.0.7

src/main/java/com/avast/server/hdfsshell/commands/ContextCommands.java

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,28 @@
44
import org.apache.hadoop.fs.FileStatus;
55
import org.apache.hadoop.fs.FileSystem;
66
import org.apache.hadoop.fs.Path;
7+
import org.apache.hadoop.hdfs.HdfsConfiguration;
8+
import org.apache.hadoop.hdfs.NameNodeProxies;
79
import org.apache.hadoop.security.UserGroupInformation;
10+
import org.apache.hadoop.tools.GetUserMappingsProtocol;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
813
import org.springframework.shell.core.CommandMarker;
914
import org.springframework.shell.core.annotation.CliAvailabilityIndicator;
1015
import org.springframework.shell.core.annotation.CliCommand;
1116
import org.springframework.shell.core.annotation.CliOption;
1217
import org.springframework.stereotype.Component;
1318
import org.springframework.util.StringUtils;
1419

20+
import javax.annotation.PostConstruct;
1521
import java.io.IOException;
1622
import java.util.Arrays;
23+
import java.util.stream.Collectors;
1724

1825
@SuppressWarnings("SameParameterValue")
1926
@Component
2027
public class ContextCommands implements CommandMarker {
28+
private static final Logger logger = LoggerFactory.getLogger(ContextCommands.class);
2129

2230
private String currentDir;
2331
private Configuration configuration;
@@ -26,13 +34,55 @@ public class ContextCommands implements CommandMarker {
2634
private boolean showResultCode = false;
2735
private boolean failOnError;
2836

37+
private GetUserMappingsProtocol userMappingsProtocol;
2938

30-
@CliAvailabilityIndicator({"pwd", "cd"})
39+
@PostConstruct
40+
public void init() {
41+
try {
42+
final HdfsConfiguration conf = new HdfsConfiguration();
43+
userMappingsProtocol = NameNodeProxies.createProxy(conf, FileSystem.getDefaultUri(conf),
44+
GetUserMappingsProtocol.class).getProxy();
45+
} catch (Exception e) {
46+
logger.error("Failed to create proxy to get user groups", e);
47+
}
48+
}
49+
50+
public String[] getGroupsForUser(String username) {
51+
if (userMappingsProtocol != null) {
52+
try {
53+
return userMappingsProtocol.getGroupsForUser(username);
54+
} catch (IOException e) {
55+
return new String[0];
56+
}
57+
}
58+
return new String[0];
59+
}
60+
61+
@CliAvailabilityIndicator({"pwd", "cd", "groups"})
3162
public boolean isSimpleAvailable() {
3263
//always available
3364
return true;
3465
}
3566

67+
@CliCommand(value = "groups", help = "Get groups for user")
68+
public String groups(@CliOption(key = {""}) String username) throws IOException {
69+
if (StringUtils.isEmpty(username)) {
70+
username = whoami();
71+
}
72+
final StringBuilder result = new StringBuilder();
73+
Arrays.stream(username.split("\\W+")).forEach((user) -> {
74+
if (!user.trim().isEmpty()) {
75+
user = user.trim();
76+
if (result.length() > 0) {
77+
result.append(System.lineSeparator());
78+
}
79+
result.append(user).append(" : ").append(Arrays.stream(this.getGroupsForUser(user)).collect(Collectors.joining(" ")));
80+
}
81+
}
82+
);
83+
return result.toString();
84+
}
85+
3686
@CliCommand(value = "set", help = "Set switch value")
3787
public String set(@CliOption(key = {""}, help = "showResultCodeON/showResultCodeOFF") String commandSwitch) {
3888
if (commandSwitch == null) {

src/main/java/com/avast/server/hdfsshell/ui/ShellBannerProvider.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,24 @@
1515
*/
1616
package com.avast.server.hdfsshell.ui;
1717

18+
import org.springframework.boot.ansi.AnsiColor;
19+
import org.springframework.boot.ansi.AnsiOutput;
1820
import org.springframework.core.Ordered;
1921
import org.springframework.core.annotation.Order;
2022
import org.springframework.shell.plugin.support.DefaultBannerProvider;
2123
import org.springframework.stereotype.Component;
2224

25+
import javax.annotation.PostConstruct;
26+
2327
/**
2428
* @author Jarred Li
2529
*/
2630
@Component
2731
@Order(Ordered.HIGHEST_PRECEDENCE)
2832
public class ShellBannerProvider extends DefaultBannerProvider {
2933

34+
private SimpleBashPromptInterpreter bashPromptInterpreter;
35+
3036
public String getBanner() {
3137
return "";//we are using Spring Boot for that
3238
}
@@ -36,13 +42,18 @@ public String getVersion() {
3642
return ShellBannerProvider.versionInfo();
3743
}
3844

45+
@PostConstruct
46+
public void init() {
47+
bashPromptInterpreter = new SimpleBashPromptInterpreter.Builder("HDFS-shell CLI \\h").setAddResetEnd(false).build();
48+
}
49+
3950
public String getWelcomeMessage() {
40-
return "Welcome to HDFS-shell CLI";
51+
return AnsiOutput.toString(AnsiColor.BRIGHT_GREEN, "Welcome to HDFS-shell CLI ", AnsiColor.DEFAULT);
4152
}
42-
53+
4354
@Override
4455
public String getProviderName() {
45-
return "HDFS-shell CLI";
56+
return bashPromptInterpreter.interpret();
4657
}
4758

4859
public static String versionInfo() {
Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/*
22
* Copyright 2011-2012 the original author or authors.
3-
*
3+
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
66
* You may obtain a copy of the License at
7-
*
7+
*
88
* http://www.apache.org/licenses/LICENSE-2.0
9-
*
9+
*
1010
* Unless required by applicable law or agreed to in writing, software
1111
* distributed under the License is distributed on an "AS IS" BASIS,
1212
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -16,42 +16,106 @@
1616
package com.avast.server.hdfsshell.ui;
1717

1818
import com.avast.server.hdfsshell.commands.ContextCommands;
19+
import org.apache.commons.lang3.StringUtils;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
1922
import org.springframework.beans.factory.annotation.Autowired;
20-
import org.springframework.boot.ansi.AnsiColor;
21-
import org.springframework.boot.ansi.AnsiOutput;
2223
import org.springframework.core.Ordered;
2324
import org.springframework.core.annotation.Order;
2425
import org.springframework.shell.core.AbstractShell;
2526
import org.springframework.shell.plugin.support.DefaultPromptProvider;
2627
import org.springframework.stereotype.Component;
2728

29+
import javax.annotation.PostConstruct;
30+
import java.io.IOException;
31+
import java.util.Arrays;
32+
import java.util.Locale;
33+
import java.util.Set;
34+
import java.util.stream.Collectors;
35+
2836
/**
29-
* @author Jarred Li
37+
* @author Vitasek L.
3038
*/
3139
@Component
3240
@Order(Ordered.HIGHEST_PRECEDENCE)
3341
public class ShellPromptProvider extends DefaultPromptProvider {
3442

43+
private static final Logger logger = LoggerFactory.getLogger(ShellPromptProvider.class);
44+
45+
private static final String DEFAULT_PROMPT = "\033[36m\\u@\\h \033[0;39m\033[33m\\w\033[0;39m\033[36m\\$ \033[37;0;39m";
3546
final ContextCommands contextCommands;
3647

48+
private SimpleBashPromptInterpreter simpleBashPromptInterpreter;
49+
3750
@Autowired
3851
public ShellPromptProvider(ContextCommands contextCommands) {
3952
this.contextCommands = contextCommands;
4053
}
4154

55+
@PostConstruct
56+
public void init() {
57+
setPs1(System.getenv().getOrDefault("HDFS_SHELL_PROMPT", DEFAULT_PROMPT));
58+
}
59+
60+
public void setPs1(String ps1) {
61+
simpleBashPromptInterpreter = new SimpleBashPromptInterpreter.Builder(ps1).
62+
setAppName(() -> "hdfs-shell").setAppVersion(ShellBannerProvider::versionInfo).
63+
setLocale(Locale.ENGLISH).
64+
setUsername(this::getWhoami).
65+
setIsRoot(this::isRootPrompt).
66+
setCwdAbsolut(contextCommands::getCurrentDir).
67+
setCwdShort(this::getShortCwd).
68+
build();
69+
}
70+
71+
private boolean isRootPrompt() {
72+
final String whoami = this.getWhoami();
73+
final String[] groupsForUser = contextCommands.getGroupsForUser(whoami);
74+
if (groupsForUser.length == 0) { //make guess
75+
return "root".equals(whoami) || "hdfs".equals(whoami);
76+
}
77+
final String[] groups = contextCommands.getConfiguration().get("dfs.permissions.superusergroup", "supergroup").split(",");
78+
final Set<String> adminGroups = Arrays.stream(groups).map(String::trim).collect(Collectors.toSet());
79+
adminGroups.add("Administrators");//for Windows
80+
adminGroups.add("hdfs");//special cases
81+
adminGroups.add("root");
82+
return Arrays.stream(groupsForUser).anyMatch(adminGroups::contains);
83+
}
4284

4385
@Override
4486
public String getPrompt() {
45-
return AbstractShell.shellPrompt =
46-
AnsiOutput.toString(AnsiColor.CYAN, "hdfs-shell ") +
47-
AnsiOutput.toString(AnsiColor.YELLOW, contextCommands.getCurrentDir()) +
48-
AnsiOutput.toString(AnsiColor.CYAN, " >", AnsiColor.WHITE);
87+
return AbstractShell.shellPrompt = simpleBashPromptInterpreter.interpret();
88+
}
89+
90+
private String getWhoami() {
91+
try {
92+
return contextCommands.whoami();
93+
} catch (IOException e) {
94+
logger.error("Failed to get active user", e);
95+
return "hdfs-shell";
96+
}
4997
}
5098

99+
// private String defaultPrompt() {
100+
// return AnsiOutput.toString(AnsiColor.CYAN, "hdfs-shell ") +
101+
// AnsiOutput.toString(AnsiColor.YELLOW, contextCommands.getCurrentDir()) +
102+
// AnsiOutput.toString(AnsiColor.CYAN, " >", AnsiColor.WHITE);
103+
// }
104+
51105

52106
@Override
53107
public String getProviderName() {
54108
return "Hdfs-shell prompt";
55109
}
56110

111+
private String getShortCwd() {
112+
String currentDir = contextCommands.getCurrentDir();
113+
if (currentDir.startsWith("/user/")) {
114+
final String userHome = "/user/" + this.getWhoami();//call getWhoami later
115+
if (currentDir.startsWith(userHome)) {
116+
currentDir = StringUtils.replaceOnce(currentDir, userHome, "~");
117+
}
118+
}
119+
return currentDir;
120+
}
57121
}

0 commit comments

Comments
 (0)